<?php
/**
 * This is the main file for the PiePHP framework and should be included
 * each site's index.php file. 
 *
 * This file can be included in app's index file in the normal way:
 *    require dirname(dirname(dirname(dirname(__FILE__)))) . '/classes/Pie.php';
 *    Pie::setSitePath([sitename]);
 * if the app is in the "normal" sites/[site name] location. Otherwise, point 
 * this to wherever PiePHP resides and where the separate site files are.
 *    require '/path/to/classes/Pie.php';
 *    Pie::setSitePath('path/to/[sitename]');
 *
 * @author     Bill Lee <>
 * @package    PiePHP
 * @since      Version 0.1
 * @copyright  Copyright (c) 2011, Pie Software Foundation
 * @license    http://www.piephp.com/license
 *
 * @todo Add autoload/spl_autoload_register handling here.
 * @todo Use getPiePath() rather than $PIE_DIR
 * @todo Add config settings here.
 * @todo Add "debug" setting here.
 * @todo Consider adding 404 error handling here?
 * @todo Figure out problem with onError() 
 * @todo Consider get/setWebPath() :: getSitePath()/public
 * @todo Add route-mapping capability -- i.e., specific URI -->specific 
 *    controller/action.
 * @todo Consider the advantages of making Pie a live singleton instance. 
 */

// $PIE_DIR is the directory which contains PiePHP libraries and sites that
// use PiePHP. 
if (!isset($PIE_DIR))
	Pie::setPiePath(dirname(dirname(__FILE__)) . '/');
else 
	Pie::setPiePath($PIE_DIR);

// We are using spl_autoload_* features to simplify search for class files. If
// the app has defined an __autoload() of their own without chaining it with
// the spl_autoload_register() call, then this will add it automatically.
if (function_exists('__autoload')) {
//echo basename(__FILE__) . '#' . __LINE__." register __autoload<br/>";
	spl_autoload_register('__autoload', true, true); 
}
spl_autoload_register('Pie::autoload');
 
/**
 * The PiePHP Framework class. This is a "static" class.
 * @see dispatch()... that's where it all begins.
 * @TODO: Gather config settings, here. 
 */
class Pie 
{
	protected static $_piepath=NULL;		// Path to PiePHP framework files.
	protected static $_sitepath=NULL;		// Path to site specific files.

	protected static $actionName=NULL;		// Current Action
	protected static $currentController=NULL; // Currently running controller
	protected static $params=NULL; 			// Current parameter set
	protected static $config=NULL;			// Configuration array

	private function __construct() {} 		// block from instantiation
	
	/**
	 * Search for and load Pie and site classes. This method is appropriate to
	 * call this from __autoload() or register via spl_autoload_register()
	 */
	public static function autoload($className)
	{
		global $SITE_DIR;

// echo __METHOD__.'#'.__LINE__. ' className = ' . $className . '<br/>'.PHP_EOL;
		$autoloadFile = 'classes/' . str_replace('_', '/', $className) . '.php';

		// Call to getSitePath() rather than $_sitepath to make sure it's up to 
		// date while we still rely on $SITE_DIR.
		if (file_exists(self::getSitePath() . $autoloadFile)) {
//echo __METHOD__.'#'.__LINE__. self::$_sitepath . $autoloadFile  . '<br/>';
			include(self::$_sitepath . $autoloadFile);
		}
		// Call to getPiePath() rather than $_piepath to make sure it's up to 
		// date while we still rely on $PIE_DIR.
		elseif (file_exists(self::getPiePath() . $autoloadFile)) {
// echo __METHOD__.'#'.__LINE__. self::$_piepath . $autoloadFile  . '<br/>';
			include(self::$_piepath . $autoloadFile);
		}
	} // autoload()

	/**
	 * @todo Move this to Controller; then we can encapsulate $ACTION_NAME (and,
	 *       perhaps $PARAMETERS) there as well. ...perhaps... perhaps not, 
	 *       because we also need to keep "routing" algorithm at the top-level,
	 *       within mapRoute().
	 */
	public static function buildActionName($controller) {
		global $ACTION_NAME;		// Set
		global $PARAMETERS;			// Set
		$ACTION_NAME = (count($PARAMETERS) ? lower_camel($PARAMETERS[0]) : '') . 'Action';
// echo __METHOD__.'#'.__LINE__.' ActionName = '.$ACTION_NAME.'<br/>';

		// If the URL was "/something/hello/" then we want the helloAction of
		// the SomethingController.  If the SomethingController doesn't have a
		// helloAction, it might have a catchAllAction.  If not, then the base
		// Controller catchAllAction method can return a 404.
		if ($ACTION_NAME == 'Action') {
			$ACTION_NAME = 'defaultAction';
		}		
		
		if (!$controller->hasAction($ACTION_NAME)) {
			$ACTION_NAME = 'catchAllAction';
		}
		else {
			array_shift($PARAMETERS);
		}
		return $ACTION_NAME;
	} // buildActionName()
		
	/**
	 * Calls controller/action.
	 */
	public static function dispatch() 
	{
		// If we're not posting data, we should check for a cached copy of the requested page.
		if (!count($_POST) && isset($CACHES['pages'])) {
			self::displayFromCache();
		}
		else {
			global $NOTIFICATIONS;		// Set

			// TODO: do something better than just checking for a closing brace to verify the unserializability.
			if (isset($_COOKIE['notifications']) && strpos($_COOKIE['notifications'], '}') !== false) {
				$NOTIFICATIONS = unserialize($_COOKIE['notifications']);
			}
			setcookie('notifications', '', time(), '/');

			// Output buffering must be turned on in order to support
			// post-rendering source modifications.
			// TODO: Find out why PHP won't let me get the contents of a gzipped buffer.
			ob_start();
		
			self::setupEnvFromUri();	// Make sure env is set based on URL
			
			self::mapRoute();			// Determine controller/action
// echo __METHOD__.'#'.__LINE__.' ControllerName = '.get_class(self::$currentController).'<br/>';
// echo __METHOD__.'#'.__LINE__.' ActionName = '.self::$actionName.'<br/>';
// echo __METHOD__.'#'.__LINE__.' Pararameters = '.print_r(self::$params, true).'<br/>';
			self::run();				// Dispatch to controller/action
			
			exit;
		}
	} // dispatch()
	
	/**
	 * Retrieve and display from cache.
	 */
	protected static function displayFromCache() {
		global $CACHES;		// Referenced
			
		// Call to getPiePath() rather than $_piepath to make sure it's up to 
		// date while we still rely on $PIE_DIR.
		include self::getPiePath() . 'classes/Model.php';
		include self::$_piepath . 'classes/' . ($CACHES['pages'][0] == 'f' ? 'File' : 'Memcache') . 'Cache.php';
		$pageModel = new Model();
		$pageModel->cacheConfigName = 'pages';
		$pageModel->loadCache();
		$pageCacheKey = $_SERVER['REQUEST_URI'] . '&'
		. (is_ajax() ? 'a' : '')
		. (is_dialog() ? 'd' : '')
		. (is_https() ? 'h' : '');
		$contents = $pageModel->cache->get($pageCacheKey);
		if ($contents) {
			ob_start('ob_gzhandler');
			send_output($contents);
		}
	} // displayFromCache()
	
	/**
	 * Determine the controller, action, and parameters to be "executed"
	 */
	protected static function mapRoute()
	{
		global $PARAMETERS;			// Set
		global $URL_ROOT;			// Referenced
		global $URL_PATH;			// Referenced

		$PARAMETERS = explode('/', substr($URL_PATH, strlen($URL_ROOT)));

		$controller = upper_camel($PARAMETERS[0]) . 'Controller';
// echo __METHOD__.'#'.__LINE__.' ControllerName = '.$controller.'<br/>';

		// If the URL was "/" or if it was "/hello/" and there's no
		// HelloController, we'll just use the DefaultController.  In the
		// "/hello/" case, there's a chace that DefaultController has a
		// helloAction or a catchAllAction.
		if ($controller == 'Controller' || !@class_exists($controller, true)) {
			$controller = 'DefaultController';
		}
		else {
			array_shift($PARAMETERS);
		}
// echo __METHOD__.'#'.__LINE__.' ControllerName = '.$controller.'<br/>';
		
		self::$currentController = new $controller();		
		self::$actionName = self::buildActionName(self::$currentController);

		self::$params = $PARAMETERS;
	} // mapRoute()
	
	/**
	 * Execute action and render
	 */
	protected static function run() {
		global $MINIFY;
		global $ERRORS_CONTROLLER;
		global $NEED_TITLE;
		global $DEFAULT_TITLE;

		call_user_func_array(
			array(&self::$currentController, self::$actionName), 
			self::$params);

		$contents = trim(ob_get_clean());

		// If there was an error, then don't minify.
		if (!isset($ERRORS_CONTROLLER) && $MINIFY ) {
			$contents = minify($contents);
		}
		if (isset($NEED_TITLE)) {
			if (!isset($DEFAULT_TITLE)) {
				$DEFAULT_TITLE = 'PiePHP';
			}
			$title = $DEFAULT_TITLE;
			if (preg_match('/<h1[^>]*>(.*?)<\/h1>/ms', $contents, $match)) {
				$title .= ' - ' . $match[1];
			}
			$contents = str_replace('>NEED_TITLE<', '>' . $title . '<', $contents);
		}
		if (self::$currentController->useCaching && isset($pageModel)) {
			$pageModel->cache->set($pageCacheKey, $contents, self::$currentController->cacheTimeInSeconds);
		}
		send_output($contents);
	} // run()
	
	/**
	 * Set values based on current request URI! Ugh, globals... we'll have to get
	 * rid of most of these.
	 * @todo: Get rid of this method and all of its globals.
	 * @todo: Use parse_url()?
	 */
	protected static function setupEnvFromUri()
	{
		global $URL_ROOT;		// Referenced
		global $URL_PATH;		// Set
		global $QUERY_STRING;	// Set
		global $HTTP_BASE;		// Set
		global $HTTPS_BASE;		// Set
		global $HTTP_ROOT;		// Set
		global $HTTPS_ROOT;		// Set
		
		// The REQUEST_URI is what was requested before mod_rewrite changed anything.
		// For a URL like "http://server:port/url/to/the/page?query=string",
		// the URL_ROOT will be "/url/to/the/page".
		// and the QUERY_STRING will be "query=string".
		list($URL_PATH, $QUERY_STRING) = explode('?', $_SERVER['REQUEST_URI'] . '?');
		if (strpos($URL_PATH, $URL_ROOT) === false) {
			$URL_PATH = str_replace('//', '/', $URL_ROOT . $URL_PATH);
		}

		// If mod_rewrite used the path as a query string, we need to separate path data from query data.
		if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
			$pairs = explode('&', $QUERY_STRING);
			foreach ($pairs as $pair) {
				list($key, $value) = explode('=', $pair . '=');
				// TODO: Deal with multiple instances of the same key.
				$_GET[$key] = $value;
				$_REQUEST[$key] = $value;
			}
		}

		$HTTP_BASE = 'http://' . $_SERVER['SERVER_NAME'];
		$HTTPS_BASE = 'http://' . $_SERVER['SERVER_NAME'];
		if (is_https()) {
			$HTTP_ROOT = $HTTP_BASE . $URL_ROOT;
			$HTTPS_ROOT = $URL_ROOT;
		}
		else {
			$HTTP_ROOT = $URL_ROOT;
			$HTTPS_ROOT = $HTTPS_BASE . $URL_ROOT;
		}
	} // setupEnvFromUri()
	
	/**
	 * @return string Path to PiePHP framework library.
	 *
	 * @note For now, we always set the member data from the global $PIE_DIR
	 *    until we've gotten rid of $PIE_DIR; this is for backwards compatibility.
	 */
	public static function getPiePath() { 
		global $PIE_DIR;
		return self::$_piepath=$PIE_DIR;
	} // getPiePath()
		
	/** 
	 * Set the base path for the location of PiePHP. This should be self-evident,
	 * It is called from this file, to initialize the setting and I cannot think
	 * of any other reason an app would need to call this, but it is here for 
	 * for oompleteness anyway. 
	 *
	 * @param string $path The absolute path to the PiePHP library. 
	 *
	 * @note For now, we always set the member data from the global $PIE_DIR
	 *    until we've gotten rid of $PIE_DIR; this is for backwards compatibility.
	 * @todo Get rid of $PIE_DIR
	 */
	public static function setPiePath($path) {
		global $PIE_DIR;
		if (substr($path,-1) != DIRECTORY_SEPARATOR)
			$path .= DIRECTORY_SEPARATOR;

		self::$_piepath=$PIE_DIR=$path;//str_replace('\\', '/',$path); 
	} // setPiePath()
	
	/**
	 * @return string Path to site's sources (not just the public ones).
	 *
	 * @note For now, we always set the member data from the global $SITE_DIR
	 *    until we've gotten rid of $SITE_DIR; this is for backwards compatibility.
	 * @todo Get rid of $SITE_DIR
	 */
	public static function getSitePath() { 
		global $SITE_DIR;
		return self::$_sitepath=$SITE_DIR; 
	} // getSitePath() 
		
	/** 
	 * Set the base path for the location of app's source files. 
	 *
	 * @param string $path The absolute path to the apps source files (normally
	 *    getPiePath()/sites/[site_name]). If not an aboslute path, then it is 
	 ^    assumed to be relative to the 'sites' directory, i.e., the "name" of 
	 *    the site.
	 *
	 * @note For now, we always set the member data from the global $SITE_DIR
	 *    until we've gotten rid of $SITE_DIR; this is for backwards compatibility.
	 * @todo Get rid of $SITE_DIR
	 */
	public static function setSitePath($path) {
		global $SITE_DIR;
		// If an absolute path is not specified, assume relative to 'sites'
		if ($path[0] != DIRECTORY_SEPARATOR) 
			$path = self::getPiePath() . 'sites' . DIRECTORY_SEPARATOR . $path;
		if (substr($path,-1) != DIRECTORY_SEPARATOR)
			$path .= DIRECTORY_SEPARATOR;

		self::$_sitepath=$SITE_DIR=str_replace('\\', '/',$path); 
	} // setSitePath()
	
	/**
	 *
	 */
	public static function onError($level, $message, $file, $lineNumber, $context) {
		// Ignore certain warnings.
		if ($level == 2) {
			// /* We failed to find a class file, but we might have just been checking if a class exists. */
			if (isset($context['autoloadFile'])) {
				return;
			}
		}
		global $ERRORS_CONTROLLER;
		if (!$ERRORS_CONTROLLER) {
			$ERRORS_CONTROLLER = new ErrorsController();
		}
		$ERRORS_CONTROLLER->handleError($level, $message, $file, $lineNumber, $context);
		return true;
	} // onError()
	
} // class Pie

/**
 * When HTML has come from the cache, it won't yet contain stuff that's specific to a signed-in
 * user (such as "Welcome, Sam" and "Sign out").  So if the user is signed in, we should decorate
 * the cached HTML with user stuff. Then we'll output it through the output buffer's gzip handler.
 * @param  $output: the output to decorate and send.
 */
function send_output($output) {
	global $URL_ROOT;
	global $NOTIFICATIONS;
	if (!is_ajax()) {
		$session = new Session();
		if ($session->isSignedIn) {
			$pieces = preg_split('/<div id="user">.*?<\/div>/msi', $output, 2);
			if (count($pieces) > 1) {
				$isAdmin = count(array_intersect($session->userGroups, array(
					1, // System administrators
					2, // Developers
					3, // Administrators
				)));
				$output = $pieces[0] .
					'<div id="user">' .
						'<span>' . htmlentities($session->username) . '</span>' .
						'<u id="userNav">' .
							($isAdmin ? '<a href="' . $URL_ROOT . 'admin/">Admin</a>' : '') .
							'<a href="' . $URL_ROOT . 'sign_out/" class="noAjax">Sign out</a>' .
						'</u>' .
					'</div>' .
				$pieces[1];
			}
		}
	}
	if (count($NOTIFICATIONS)) {
		$blocks = '';
		foreach ($NOTIFICATIONS as $notification) {
			list($type, $message) = explode(' ', $notification . ' ', 2);
			$blocks .= '<h4 class="' . $type . '">' . $message . '</h4>';
		}
		$output = str_replace('<var>NOTIFICATIONS</var>', $blocks, $output);
	}
	echo $output;
	exit;
}

/**
 * Whether a page was requested via AJAX.
 * @return true if the page was requested via AJAX.
 */
function is_ajax() {
	return isset($_REQUEST['isAjax']);
}

/**
 * Whether a page is being viewed in a veil dialog.
 * @return true if the page was requested via a veil dialog.
 */
function is_dialog() {
	return isset($_REQUEST['isDialog']);
}

/**
 * Whether a page was reached via a submitter iframe.
 * @return true if the page was requested via AJAX.
 */
function is_frame() {
	return isset($_REQUEST['isFrame']);
}

/**
 * Whether the protocol is HTTPS.
 * @return true if the protocol is HTTPS.
 */
function is_https() {
	return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off';
}

/**
 * Whether the page is being viewed on a mobile device.
 * @return true if it is being viewed on a mobile device.
 */
function is_mobile() {
	strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false
		|| strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false
		|| strpos($_SERVER['HTTP_USER_AGENT'], 'Blackberry') !== false;
}

/**
 * Use a phrase in the base language to find a phrase in the target language.
 * @return a phrase in the target language.
 * TODO: Apply the arguments programmatically.
 */
function say($phrase, $contextOrSubstitutions = '', $substitutions = array()) {
	global $INTERNATIONALIZATION_SERVICE;
	if (!$INTERNATIONALIZATION_SERVICE) {
		$INTERNATIONALIZATION_SERVICE = new InternationalizationService();
	}
	return $INTERNATIONALIZATION_SERVICE->say($phrase, $contextOrSubstitutions, $substitutions);
}

/**
 * Compress HTML
 * @param string $buffer HTML text
 * @returns Compressed HTML text
 * @todo Strip JS comments out so that we can then compress the JS
 * @todo Ensure that minification doesn't mess up content inside <pre> tags, then reenable error minification.
 */
function minify($buffer) {
	$search = array(
		'/<!--.*?-->/ms',			// Strip multi-line comments ('/<!--(.|\s)*?-->/')
		'/[ \t]{2,}/m',			// Strip multi-whitespace
		'/[ \t]*[\r\f\n]+[ \t]*/m',	// Collapse multi-blank lines
		'/>[\\r\\n\\t]+</ms',	// Strip whitespace between tags
//		'@(<script[^>]*>[^\/]*)//[^\n]*$@mi',
//		'/\s+/ms'
	);
	$rep = array(
		'','',
		"\n",
		'><',
//		'$1'
//		' '
	);

	$buffer = preg_replace($search, $rep, $buffer);
//	$buffer = preg_replace('/\\s+/ms', ' ', $buffer);
	return $buffer;
} // minify()

/**
 * Convert a name separated by underscores (or other non-alphanumerics) to lowerCamelCase.
 * @param  $underscored: the name that is separated by underscores.
 * @return the name in lowerCamelCase.
 */
function lower_camel($underscored) {
	$upperCamel = upper_camel($underscored);
	if ($upperCamel) {
		$upperCamel[0] = strtolower($upperCamel[0]);
	}
	return $upperCamel;
}

/**
 * Separate a camel case name into words using separators such as underscores.
 * @param  $camel: the name in upper or lower camel case.
 * @param  $separator: the separator that we want to insert between words.
 * @return the separated words as a string.
 */
function separate($camel, $separator = '_') {
	$camel = preg_replace('/[^a-zA-Z0-9]+/', $separator, $camel);
	$separated = preg_replace('/([a-z])([A-Z])/', '$1' . $separator . '$2', $camel);
	return strtolower($separated);
}

/**
 * Convert a name separated by underscores (or other non-alphanumerics) to UpperCamelCase.
 * @param  $underscored: the name that is separated by underscores.
 * @return the name in UpperCamelCase.
 */
function upper_camel($underscored) {
	$spaced = preg_replace('/[^A-Za-z0-9]+/', ' ', $underscored);
	$cased = ucwords($spaced);
	$camel = str_replace(' ', '', $cased);
	return $camel ? $camel : '';
}

function HelperPackedInclude($type,$group) {
	global $ENVIRONMENT;
	global $URL_ROOT;
	if ($ENVIRONMENT == 'production') {
		global $VERSION;
		switch ($type) {
		case 'js':
			printf('<script type="text/javascript" src="%sjs/%s-%s.js"></script>',
				$URL_ROOT, $group, $VERSION);
			break;
		case 'css':
			printf('<link rel="stylesheet" href="%scss/%s-%s.css" type="text/css"/>',
				$URL_ROOT, $group, $VERSION);
			break;
		default:
			throw new Exception("Unsupported include type \'$type\'.");
		}
	}
	else {
		global $SITE_DIR;
		include $SITE_DIR . 'mediaConfig.php';

// echo '<pre>';
// var_dump($MEDIA_GROUPS['js']['core']); 
// echo '</pre>';
		foreach ($MEDIA_GROUPS[$type][$group] as $file) {
			switch ($type) {
			case 'js':
				printf('<script type="text/javascript" src="%sjs/%s"></script>',
					$URL_ROOT, $file);
				break;
			case 'css':
				printf('<link rel="stylesheet" href="%scss/%s" type="text/css"/>',
					$URL_ROOT, $file);
				break;
			default:
				throw new Exception("Unsupported include type \'$type\'.");
			}
		}
	}
} // HelperPackedInclude()

